فارسی

چندریسمانی واقعی را در جاوااسکریپت فعال کنید. این راهنمای جامع SharedArrayBuffer، Atomics، Web Workers و الزامات امنیتی برای برنامه‌های وب با کارایی بالا را پوشش می‌دهد.

SharedArrayBuffer در جاوااسکریپت: نگاهی عمیق به برنامه‌نویسی همزمان در وب

برای دهه‌ها، ماهیت تک‌ریسمانی (single-threaded) جاوااسکریپت هم منبع سادگی آن و هم یک گلوگاه عملکردی مهم بوده است. مدل حلقه رویداد (event loop) برای اکثر وظایف مبتنی بر رابط کاربری به زیبایی کار می‌کند، اما زمانی که با عملیات محاسباتی سنگین مواجه می‌شود، دچار مشکل می‌شود. محاسبات طولانی‌مدت می‌توانند مرورگر را قفل کرده و تجربه کاربری ناخوشایندی ایجاد کنند. در حالی که Web Workers با اجازه دادن به اجرای اسکریپت‌ها در پس‌زمینه یک راه‌حل جزئی ارائه دادند، اما با محدودیت بزرگ خودشان همراه بودند: ارتباط ناکارآمد داده‌ها.

اینجاست که SharedArrayBuffer (SAB) وارد می‌شود؛ یک ویژگی قدرتمند که با معرفی اشتراک‌گذاری حافظه واقعی و سطح پایین بین ریسمان‌ها در وب، اساساً قواعد بازی را تغییر می‌دهد. SAB در کنار شیء Atomics، عصر جدیدی از برنامه‌های کاربردی با کارایی بالا و همزمان را مستقیماً در مرورگر باز می‌کند. با این حال، با قدرت زیاد، مسئولیت—و پیچیدگی—زیادی نیز به همراه می‌آید.

این راهنما شما را به سفری عمیق در دنیای برنامه‌نویسی همزمان در جاوااسکریپت خواهد برد. ما بررسی خواهیم کرد که چرا به آن نیاز داریم، SharedArrayBuffer و Atomics چگونه کار می‌کنند، ملاحظات امنیتی حیاتی که باید به آن‌ها توجه کنید، و مثال‌های عملی برای شروع.

دنیای قدیم: مدل تک‌ریسمانی جاوااسکریپت و محدودیت‌های آن

قبل از اینکه بتوانیم راه‌حل را درک کنیم، باید مشکل را به طور کامل بفهمیم. اجرای جاوااسکریپت در یک مرورگر به طور سنتی روی یک ریسمان واحد، که اغلب "ریسمان اصلی" یا "ریسمان UI" نامیده می‌شود، اتفاق می‌افتد.

حلقه رویداد (The Event Loop)

ریسمان اصلی مسئول همه‌چیز است: اجرای کد جاوااسکریپت شما، رندر کردن صفحه، پاسخ به تعاملات کاربر (مانند کلیک و اسکرول) و اجرای انیمیشن‌های CSS. این ریسمان وظایف را با استفاده از یک حلقه رویداد مدیریت می‌کند که به طور مداوم صفی از پیام‌ها (وظایف) را پردازش می‌کند. اگر یک وظیفه زمان زیادی برای تکمیل شدن نیاز داشته باشد، کل صف را مسدود می‌کند. هیچ کار دیگری نمی‌تواند انجام شود—رابط کاربری قفل می‌شود، انیمیشن‌ها دچار لکنت می‌شوند و صفحه غیرپاسخگو می‌شود.

Web Workers: گامی در مسیر درست

Web Workers برای کاهش این مشکل معرفی شدند. یک Web Worker اساساً یک اسکریپت است که روی یک ریسمان پس‌زمینه جداگانه اجرا می‌شود. شما می‌توانید محاسبات سنگین را به یک worker منتقل کنید و ریسمان اصلی را برای مدیریت رابط کاربری آزاد نگه دارید.

ارتباط بین ریسمان اصلی و یک worker از طریق API به نام postMessage() انجام می‌شود. وقتی داده‌ای را ارسال می‌کنید، توسط الگوریتم کلون ساختاریافته (structured clone algorithm) مدیریت می‌شود. این به این معنی است که داده‌ها سریال‌سازی، کپی و سپس در زمینه worker دسریال‌سازی می‌شوند. با اینکه این روش مؤثر است، اما برای مجموعه‌داده‌های بزرگ معایب قابل توجهی دارد:

یک ویرایشگر ویدیو در مرورگر را تصور کنید. ارسال یک فریم کامل ویدیو (که می‌تواند چندین مگابایت باشد) به یک worker و بازگرداندن آن برای پردازش ۶۰ بار در ثانیه، هزینه‌ای سرسام‌آور خواهد داشت. این دقیقاً همان مشکلی است که SharedArrayBuffer برای حل آن طراحی شده است.

تغییردهنده بازی: معرفی SharedArrayBuffer

یک SharedArrayBuffer یک بافر داده باینری خام با طول ثابت است، شبیه به یک ArrayBuffer. تفاوت حیاتی این است که یک SharedArrayBuffer می‌تواند بین چندین ریسمان (مانند ریسمان اصلی و یک یا چند Web Worker) به اشتراک گذاشته شود. وقتی شما یک SharedArrayBuffer را با استفاده از postMessage() "ارسال" می‌کنید، شما یک کپی ارسال نمی‌کنید؛ شما یک مرجع به همان بلوک از حافظه را ارسال می‌کنید.

این بدان معناست که هر تغییری که توسط یک ریسمان در داده‌های بافر ایجاد شود، فوراً برای تمام ریسمان‌های دیگری که به آن مرجع دارند، قابل مشاهده است. این کار مرحله پرهزینه کپی و سریال‌سازی را حذف می‌کند و اشتراک‌گذاری داده تقریباً آنی را ممکن می‌سازد.

این‌گونه به آن فکر کنید:

خطر حافظه مشترک: شرایط رقابتی (Race Conditions)

اشتراک‌گذاری آنی حافظه قدرتمند است، اما یک مشکل کلاسیک از دنیای برنامه‌نویسی همزمان را نیز معرفی می‌کند: شرایط رقابتی.

یک شرایط رقابتی زمانی رخ می‌دهد که چندین ریسمان سعی می‌کنند به طور همزمان به یک داده مشترک دسترسی پیدا کرده و آن را تغییر دهند، و نتیجه نهایی به ترتیب غیرقابل پیش‌بینی اجرای آن‌ها بستگی دارد. یک شمارنده ساده که در یک SharedArrayBuffer ذخیره شده را در نظر بگیرید. هم ریسمان اصلی و هم یک worker می‌خواهند آن را افزایش دهند.

  1. ریسمان A مقدار فعلی را که ۵ است، می‌خواند.
  2. قبل از اینکه ریسمان A بتواند مقدار جدید را بنویسد، سیستم عامل آن را متوقف کرده و به ریسمان B سوئیچ می‌کند.
  3. ریسمان B مقدار فعلی را که هنوز ۵ است، می‌خواند.
  4. ریسمان B مقدار جدید (۶) را محاسبه کرده و آن را در حافظه می‌نویسد.
  5. سیستم به ریسمان A بازمی‌گردد. او نمی‌داند که ریسمان B کاری انجام داده است. از همان جایی که متوقف شده بود، ادامه می‌دهد، مقدار جدید خود (۵ + ۱ = ۶) را محاسبه کرده و ۶ را در حافظه می‌نویسد.

با وجود اینکه شمارنده دو بار افزایش یافت، مقدار نهایی ۶ است، نه ۷. عملیات‌ها اتمیک نبودند—آن‌ها قابل прерывание بودند و منجر به از دست رفتن داده‌ها شدند. این دقیقاً دلیلی است که شما نمی‌توانید از SharedArrayBuffer بدون شریک حیاتی آن، یعنی شیء Atomics، استفاده کنید.

نگهبان حافظه مشترک: شیء Atomics

شیء Atomics مجموعه‌ای از متدهای استاتیک برای انجام عملیات اتمیک بر روی اشیاء SharedArrayBuffer فراهم می‌کند. یک عملیات اتمیک تضمین می‌شود که به طور کامل و بدون прерывание توسط هیچ عملیات دیگری انجام شود. یا به طور کامل اتفاق می‌افتد یا اصلاً اتفاق نمی‌افتد.

استفاده از Atomics با تضمین اینکه عملیات‌های خواندن-تغییر-نوشتن بر روی حافظه مشترک به صورت ایمن انجام می‌شوند، از شرایط رقابتی جلوگیری می‌کند.

متدهای کلیدی Atomics

بیایید به برخی از مهم‌ترین متدهای ارائه شده توسط Atomics نگاهی بیندازیم.

همگام‌سازی: فراتر از عملیات ساده

گاهی اوقات شما به چیزی بیشتر از خواندن و نوشتن ایمن نیاز دارید. شما نیاز دارید که ریسمان‌ها هماهنگ شوند و منتظر یکدیگر بمانند. یک ضدالگوی رایج "انتظار مشغول" (busy-waiting) است، جایی که یک ریسمان در یک حلقه فشرده می‌نشیند و به طور مداوم یک مکان حافظه را برای تغییر بررسی می‌کند. این کار چرخه‌های CPU را هدر می‌دهد و عمر باتری را کاهش می‌دهد.

Atomics یک راه‌حل بسیار کارآمدتر با wait() و notify() ارائه می‌دهد.

کنار هم قرار دادن همه چیز: یک راهنمای عملی

حالا که تئوری را فهمیدیم، بیایید مراحل پیاده‌سازی یک راه‌حل با استفاده از SharedArrayBuffer را طی کنیم.

مرحله ۱: پیش‌نیاز امنیتی - ایزوله‌سازی میان‌مبداء (Cross-Origin Isolation)

این رایج‌ترین مانع برای توسعه‌دهندگان است. به دلایل امنیتی، SharedArrayBuffer فقط در صفحاتی در دسترس است که در حالت ایزوله میان‌مبداء (cross-origin isolated) هستند. این یک اقدام امنیتی برای کاهش آسیب‌پذیری‌های اجرای سوداگرانه مانند Spectre است که به طور بالقوه می‌توانند از تایمرهای با وضوح بالا (که توسط حافظه مشترک ممکن شده‌اند) برای نشت داده‌ها در بین مبداها استفاده کنند.

برای فعال کردن ایزوله‌سازی میان‌مبداء، باید وب سرور خود را طوری پیکربندی کنید که دو هدر HTTP خاص را برای سند اصلی شما ارسال کند:


Cross-Origin-Opener-Policy: same-origin
Cross-Origin-Embedder-Policy: require-corp

راه‌اندازی این مورد می‌تواند چالش‌برانگیز باشد، به خصوص اگر به اسکریپت‌ها یا منابع شخص ثالثی که هدرهای لازم را ارائه نمی‌دهند، وابسته باشید. پس از پیکربندی سرور خود، می‌توانید با بررسی ویژگی self.crossOriginIsolated در کنسول مرورگر، بررسی کنید که آیا صفحه شما ایزوله شده است یا خیر. این مقدار باید true باشد.

مرحله ۲: ایجاد و اشتراک‌گذاری بافر

در اسکریپت اصلی خود، SharedArrayBuffer و یک "نما" (view) بر روی آن را با استفاده از یک TypedArray مانند Int32Array ایجاد می‌کنید.

main.js:


// ابتدا ایزوله‌سازی میان‌مبداء را بررسی کنید!
if (!self.crossOriginIsolated) {
  console.error("این صفحه ایزوله‌سازی میان‌مبداء ندارد. SharedArrayBuffer در دسترس نخواهد بود.");
} else {
  // یک بافر اشتراکی برای یک عدد صحیح ۳۲ بیتی ایجاد کنید.
  const buffer = new SharedArrayBuffer(4);

  // یک نما بر روی بافر ایجاد کنید. تمام عملیات اتمیک بر روی نما انجام می‌شود.
  const int32Array = new Int32Array(buffer);

  // مقدار را در ایندکس ۰ مقداردهی اولیه کنید.
  int32Array[0] = 0;

  // یک worker جدید ایجاد کنید.
  const worker = new Worker('worker.js');

  // بافر اشتراکی (SHARED) را به worker ارسال کنید. این یک انتقال مرجع است، نه یک کپی.
  worker.postMessage({ buffer });

  // به پیام‌های worker گوش دهید.
  worker.onmessage = (event) => {
    console.log(`Worker اتمام کار را گزارش داد. مقدار نهایی: ${Atomics.load(int32Array, 0)}`);
  };
}

مرحله ۳: انجام عملیات اتمیک در Worker

worker بافر را دریافت می‌کند و اکنون می‌تواند عملیات اتمیک را بر روی آن انجام دهد.

worker.js:


self.onmessage = (event) => {
  const { buffer } = event.data;
  const int32Array = new Int32Array(buffer);

  console.log("Worker بافر اشتراکی را دریافت کرد.");

  // بیایید چند عملیات اتمیک انجام دهیم.
  for (let i = 0; i < 1000000; i++) {
    // به صورت ایمن مقدار اشتراکی را افزایش دهید.
    Atomics.add(int32Array, 0, 1);
  }

  console.log("Worker کار افزایش را تمام کرد.");

  // به ریسمان اصلی اطلاع دهید که کار ما تمام شده است.
  self.postMessage({ done: true });
};

مرحله ۴: یک مثال پیشرفته‌تر - جمع موازی با همگام‌سازی

بیایید یک مشکل واقعی‌تر را حل کنیم: جمع کردن یک آرایه بسیار بزرگ از اعداد با استفاده از چندین worker. ما از Atomics.wait() و Atomics.notify() برای همگام‌سازی کارآمد استفاده خواهیم کرد.

بافر اشتراکی ما سه بخش خواهد داشت:

main.js:


if (self.crossOriginIsolated) {
  const NUM_WORKERS = 4;
  const DATA_SIZE = 10_000_000;

  // [status, workers_finished, result]
  // ما از دو عدد صحیح ۳۲ بیتی برای نتیجه استفاده می‌کنیم تا از سرریز برای جمع‌های بزرگ جلوگیری کنیم.
  const sharedBuffer = new SharedArrayBuffer(4 * 4); // ۴ عدد صحیح
  const sharedArray = new Int32Array(sharedBuffer);

  // مقداری داده تصادفی برای پردازش ایجاد کنید
  const data = new Uint8Array(DATA_SIZE);
  for (let i = 0; i < DATA_SIZE; i++) {
    data[i] = Math.floor(Math.random() * 10);
  }

  const chunkSize = Math.ceil(DATA_SIZE / NUM_WORKERS);

  for (let i = 0; i < NUM_WORKERS; i++) {
    const worker = new Worker('sum_worker.js');
    const start = i * chunkSize;
    const end = Math.min(start + chunkSize, DATA_SIZE);
    
    // یک نمای غیراشتراکی برای بخش داده worker ایجاد کنید
    const dataChunk = data.subarray(start, end);

    worker.postMessage({ 
      sharedBuffer,
      dataChunk // این کپی می‌شود
    });
  }

  console.log('ریسمان اصلی اکنون منتظر اتمام کار workerها است...');

  // منتظر بمانید تا پرچم وضعیت در ایندکس ۰ به ۱ تبدیل شود
  // این خیلی بهتر از یک حلقه while است!
  Atomics.wait(sharedArray, 0, 0); // اگر sharedArray[0] برابر با ۰ باشد، منتظر بمان

  console.log('ریسمان اصلی بیدار شد!');
  const finalSum = Atomics.load(sharedArray, 2);
  console.log(`جمع موازی نهایی: ${finalSum}`);

} else {
  console.error('صفحه ایزوله‌سازی میان‌مبداء ندارد.');
}

sum_worker.js:


self.onmessage = ({ data }) => {
  const { sharedBuffer, dataChunk } = data;
  const sharedArray = new Int32Array(sharedBuffer);

  // جمع را برای بخش این worker محاسبه کنید
  let localSum = 0;
  for (let i = 0; i < dataChunk.length; i++) {
    localSum += dataChunk[i];
  }

  // به صورت اتمیک جمع محلی را به کل اشتراکی اضافه کنید
  Atomics.add(sharedArray, 2, localSum);

  // به صورت اتمیک شمارنده 'workerهای تمام شده' را افزایش دهید
  const finishedCount = Atomics.add(sharedArray, 1, 1) + 1;

  // اگر این آخرین workerای است که کارش تمام می‌شود...
  const NUM_WORKERS = 4; // در یک برنامه واقعی باید این مقدار پاس داده شود
  if (finishedCount === NUM_WORKERS) {
    console.log('آخرین worker کارش تمام شد. در حال اطلاع‌رسانی به ریسمان اصلی.');

    // ۱. پرچم وضعیت را به ۱ (تکمیل شده) تغییر دهید
    Atomics.store(sharedArray, 0, 1);

    // ۲. به ریسمان اصلی که منتظر ایندکس ۰ است، اطلاع دهید
    Atomics.notify(sharedArray, 0, 1);
  }
};

موارد استفاده و کاربردهای دنیای واقعی

این فناوری قدرتمند اما پیچیده در کجا واقعاً تفاوت ایجاد می‌کند؟ این فناوری در برنامه‌هایی که نیاز به محاسبات سنگین و قابل موازی‌سازی بر روی مجموعه‌داده‌های بزرگ دارند، برتری دارد.

چالش‌ها و ملاحظات نهایی

در حالی که SharedArrayBuffer تحول‌آفرین است، اما یک راه‌حل جادویی نیست. این یک ابزار سطح پایین است که نیاز به مدیریت دقیق دارد.

  1. پیچیدگی: برنامه‌نویسی همزمان به طور بدنامی دشوار است. اشکال‌زدایی شرایط رقابتی و بن‌بست‌ها (deadlocks) می‌تواند فوق‌العاده چالش‌برانگیز باشد. شما باید به طور متفاوتی در مورد نحوه مدیریت وضعیت برنامه خود فکر کنید.
  2. بن‌بست‌ها (Deadlocks): یک بن‌بست زمانی رخ می‌دهد که دو یا چند ریسمان برای همیشه مسدود می‌شوند و هر کدام منتظر دیگری برای آزاد کردن یک منبع هستند. این اتفاق می‌تواند در صورت پیاده‌سازی نادرست مکانیزم‌های قفل‌گذاری پیچیده رخ دهد.
  3. سربار امنیتی: الزام ایزوله‌سازی میان‌مبداء یک مانع قابل توجه است. این می‌تواند ادغام با سرویس‌های شخص ثالث، تبلیغات و درگاه‌های پرداخت را در صورتی که از هدرهای CORS/CORP لازم پشتیبانی نکنند، مختل کند.
  4. برای هر مشکلی مناسب نیست: برای وظایف پس‌زمینه ساده یا عملیات I/O، مدل سنتی Web Worker با postMessage() اغلب ساده‌تر و کافی است. فقط زمانی به سراغ SharedArrayBuffer بروید که یک گلوگاه واضح و وابسته به CPU دارید که شامل مقادیر زیادی داده است.

نتیجه‌گیری

SharedArrayBuffer، در کنار Atomics و Web Workers، یک تغییر پارادایم برای توسعه وب محسوب می‌شود. این فناوری مرزهای مدل تک‌ریسمانی را در هم می‌شکند و دسته‌ای جدید از برنامه‌های قدرتمند، با کارایی بالا و پیچیده را به مرورگر دعوت می‌کند. این پلتفرم وب را برای وظایف محاسباتی سنگین در جایگاهی برابرتر با توسعه برنامه‌های بومی قرار می‌دهد.

سفر به جاوااسکریپت همزمان چالش‌برانگیز است و نیازمند یک رویکرد دقیق به مدیریت وضعیت، همگام‌سازی و امنیت است. اما برای توسعه‌دهندگانی که به دنبال پیش بردن مرزهای ممکن در وب هستند—از سنتز صوتی لحظه‌ای گرفته تا رندرینگ سه‌بعدی پیچیده و محاسبات علمی—تسلط بر SharedArrayBuffer دیگر فقط یک گزینه نیست؛ بلکه یک مهارت ضروری برای ساخت نسل بعدی برنامه‌های وب است.